/*
 * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2006.
 *
 * Licensed under the Aduna BSD-style license.
 */

package edu.tju.commons.xml;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.HashMap;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 * SAXWriter is a utility class that translates events sent to a SAX
 * ContentHandler to a character stream send to a Writer. SAXWriter optionally
 * supports indentation.
 * <P>
 * <B>Fixme:</B> usage of an SAXWriter on an OutputStream when the character
 * data contains non-ASCII codes still needs to be tested!!!
 */
public class SAXWriter implements ContentHandler {

	/**
	 * Indicates the default behaviour of an SAXWriter concerning indentation.
	 */
	public static final boolean DEFAULT_INDENT = false;

	/**
	 * The number of spaces that together form an indentation unit.
	 */
	public static final int INDENT_INCREASE = 2;

	/**
	 * A static array filled with spaces. Useful for speeding up the printing of
	 * indentation, since often only a single write method invication is
	 * sufficient for printing a whole line of indentation. Initialized by the
	 * static initializer below.
	 */
	private static final char[] SPACES;

	/**
	 * The size of the SPACES array.
	 */
	private static final int SPACES_LENGTH = 30;

	static {
		SPACES = new char[SPACES_LENGTH];
		Arrays.fill(SPACES, ' ');
	}

	/**
	 * The Writer to which the XML characters stream will be written.
	 */
	private Writer writer;

	/**
	 * Indicates whether or not this SAXWriter should use indentation.
	 */
	private boolean indenting;

	/**
	 * The number of spaces that need to be written the next time when
	 * indentation is printed.
	 */
	private int indentLength;

	/**
	 * A mapping from namespace prefixes to namespace URIs.
	 */
	private HashMap<String, String> prefixMappings;

	private String lastUri;

	private String lastLName;

	private String lastQName;

	private AttributesImpl lastAttributes;

	/**
	 * A flag that prevents printing of unwanted indentation when several
	 * subsequent invocations on the characters method are made.
	 */
	private boolean charactersNeedIndenting;

	/**
	 * Create a new SAXWriter for the specified Writer which optionally prints
	 * indentation.
	 */
	public SAXWriter(Writer writer, boolean indenting) {
		this.writer = writer;
		this.indenting = indenting;

		indentLength = 0;
		lastAttributes = new AttributesImpl();
		charactersNeedIndenting = true;
	}

	/**
	 * Create a new SAXWriter for the specified Writer with the default
	 * indentation settings.
	 */
	public SAXWriter(Writer writer) {
		this(writer, DEFAULT_INDENT);
	}

	/**
	 * Create a new SAXWriter for the specified OutputStream which optionally
	 * prints indentation. The characters will be converted to bytes using the
	 * ISO-8859-1 encoding.
	 */
	public SAXWriter(OutputStream stream, boolean indent)
		throws IOException
	{
		this(new OutputStreamWriter(stream, "ISO8859_1"), indent);
	}

	/**
	 * Create a new SAXWriter for the specified OutputStream with the default
	 * indentation property. The characters will be converted to bytes using the
	 * ISO-8859-1 encoding.
	 */
	public SAXWriter(OutputStream stream)
		throws IOException
	{
		this(stream, DEFAULT_INDENT);
	}

	public void setDocumentLocator(Locator locator) {
		// no-op
	}

	public void startDocument()
		throws SAXException
	{
		try {
			writer.write("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>");
			indentLength = 0;
			prefixMappings = new HashMap<String, String>();
		}
		catch (IOException e) {
			throw new SAXException("I/O error", e);
		}
	}

	public void endDocument()
		throws SAXException
	{
		try {
			writer.write('\n');
			writer.flush();
			prefixMappings = null;
		}
		catch (IOException e) {
			throw new SAXException("I/O error", e);
		}
	}

	public void startPrefixMapping(String prefix, String uri) {
		prefixMappings.put(prefix, uri);
	}

	public void endPrefixMapping(String prefix) {
		prefixMappings.remove(prefix);
	}

	/**
	 * Inherit comment. Printing of the specified tag and attributes is postponed
	 * until the next invocation of the startElement, endElement or characters
	 * methods in order to enable self-closing tags.
	 */
	public void startElement(String uri, String lName, String qName, Attributes attrs)
		throws SAXException
	{
		// first write out an older, cached start tag with its attributes
		if (hasCachedTag()) {
			printElement(lastUri, lastLName, lastQName, lastAttributes, false);
		}

		// cache the current start tag and its attributes
		lastUri = uri;
		lastLName = lName;
		lastQName = qName;
		if (attrs == null) {
			lastAttributes.clear();
		}
		else {
			lastAttributes.setAttributes(attrs);
		}
	}

	private boolean hasCachedTag() {
		return lastUri != null || lastLName != null || lastQName != null;
	}

	public void endElement(String uri, String lName, String qName)
		throws SAXException
	{
		if (lastUri == null && lastLName == null && lastQName == null) {
			// just write down the end tag
			try {
				if (indenting) {
					indentLength -= INDENT_INCREASE;
					printIndentation();
				}

				writer.write("</");
				writer.write(getTagName(uri, lName, qName));
				writer.write('>');
			}
			catch (IOException e) {
				throw new SAXException("I/O error", e);
			}
		}
		else {
			// write down a self-closing end-tag
			if (!equals(uri, lastUri) || !equals(lName, lastLName) || !equals(qName, lastQName)) {
				throw new SAXException("No matching start and end tags: " + getTagName(uri, lName, qName) + ", "
						+ getTagName(lastUri, lastLName, lastQName));
			}

			printElement(uri, lName, qName, lastAttributes, true);
			clearTagCache();

			// decrease the indent length
			if (indenting) {
				indentLength -= INDENT_INCREASE;
			}
		}

		charactersNeedIndenting = true;
	}

	private boolean equals(String s1, String s2) {
		if (s1 == s2) {
			// also catches "null == null"
			return true;
		}
		else if (s1 != null && s2 != null) {
			return s1.equals(s2);
		}
		else {
			return false;
		}
	}

	private void clearTagCache() {
		lastUri = null;
		lastLName = null;
		lastQName = null;
		lastAttributes.clear();
	}

	public void characters(char[] buf, int offset, int len)
		throws SAXException
	{
		// first write out a start tag, if its still open
		if (hasCachedTag()) {
			printElement(lastUri, lastLName, lastQName, lastAttributes, false);
		}

		// make a self-closing tag impossible
		clearTagCache();

		// write indentation
		if (indenting && charactersNeedIndenting) {
			try {
				printIndentation();
			}
			catch (IOException e) {
				throw new SAXException("I/O error", e);
			}

			charactersNeedIndenting = false;
		}

		try {
			int length = offset + len;
			char c;

			for (int i = offset; i < length; i++) {
				c = buf[i];

				// escape '&' and '<'
				if (c == '&') {
					writer.write("&amp;");
				}
				else if (c == '<') {
					writer.write("&lt;");
				}
				else if (c == '>') {
					// from http://www.w3.org/TR/2000/REC-xml-20001006#charsets:
					// The right angle bracket (>) may be represented using the
					// string "&gt;", and must, for compatibility, be escaped
					// using "&gt;" or a character reference when it appears in
					// the string "]]>" in content, when that string is not
					// marking the end of a CDATA section
					writer.write("&gt;");
				}
				else {
					// legal XML characters:
					// http://www.w3.org/TR/2000/REC-xml-20001006#charsets
					if (c == (char)0x9 || c == (char)0xA || c == (char)0xD
							|| (c >= (char)0x20 && c <= (char)0xD7FF) || (c >= (char)0xE000 && c <= (char)0xFFFD))
					// || (c >= (char)0x10000 && c <= (char)0x10FFFF)) // this
					// is out of range for Java chars
					{
						writer.write(c);
					}
					else {
						// You might be tempted to escape other chars like this:
						// _writer.write("&#");
						// _writer.write(Integer.toString(c));
						// _writer.write(";");

						// But it results in:
						// org.xml.sax.SAXParseException: Character reference
						// "&#18" is an invalid XML
						// character.
						// i.e. the characters outside the ranges defined above
						// MUST NOT BE USED in XML.
					}
				}
			}
		}
		catch (IOException e) {
			throw new SAXException("I/O error", e);
		}
	}

	public void ignorableWhitespace(char[] buf, int offset, int len)
		throws SAXException
	{
		// ... happily ignoring...
	}

	public void processingInstruction(String target, String data)
		throws SAXException
	{
		try {
			writer.write("<?");
			writer.write(target);
			writer.write(' ');
			writer.write(data);
			writer.write("?>");
		}
		catch (IOException e) {
			throw new SAXException("I/O error", e);
		}
	}

	public void skippedEntity(String name) {
		// ignore
	}

	private String getTagName(String uri, String lName, String qName)
		throws SAXException
	{
		String tag = qName;
		if (tag == null) {
			if (lName != null) {
				if (uri == null) {
					tag = lName;
				}
				else {
					tag = uri + ":" + lName;
				}
			}
		}

		if (tag == null) {
			throw new SAXException("Unable to determine tag");
		}

		return tag;
	}

	/**
	 * Writes a starttag with the specified name and attributes to the Writer of
	 * this class, optionally as a self-closing tag.
	 */
	private void printElement(String uri, String lName, String qName, Attributes attrs, boolean closing)
		throws SAXException
	{
		// figure out the tag name
		String tag = getTagName(uri, lName, qName);

		// write indentation
		try {
			if (indenting) {
				printIndentation();
				indentLength += INDENT_INCREASE;
			}

			// write the start tag
			writer.write('<');
			writer.write(tag);
			if (attrs != null) {
				for (int i = 0; i < attrs.getLength(); i++) {
					// determine the attribute name
					String attName = attrs.getQName(i);
					if (attName == null) {
						String attUri = attrs.getURI(i);
						String attLName = attrs.getLocalName(i);
						if (attLName != null) {
							if (attUri == null) {
								attName = attLName;
							}
							else {
								attName = attUri + ":" + attLName;
							}
						}
					}

					if (attName == null) {
						throw new SAXException("Unable to determine attribute name");
					}

					// write the attribute name and value
					writer.write(' ');
					writer.write(attName);
					writer.write("=\"");
					writer.write(XMLUtil.escapeAttributeValue(attrs.getValue(i)));
					writer.write("\"");
				}
			}

			// write the end of the start tag
			if (closing) {
				writer.write("/>");
			}
			else {
				writer.write('>');
			}
		}
		catch (IOException e) {
			throw new SAXException("I/O error", e);
		}
	}

	/**
	 * Writes a newline and "indentLength" number of spaces to the Writer of this
	 * SAXWriter.
	 */
	private void printIndentation()
		throws IOException
	{
		// write newline
		writer.write('\n');

		// write spaces
		int length = indentLength;
		while (length > 0) {
			writer.write(SPACES, 0, length);
			length -= SPACES_LENGTH;
		}
	}
}
